DearMiku

YYAsyncLayer 学习

字数统计: 2k阅读时长: 9 min
2017/11/23 Share

YYAsyncLayer 学习

简介

YYAsyncLayer是用于图层异步绘制的一个组件,将耗时操作(如文本布局计算)放在RunLoop空闲时去做,进而减少卡顿.

组件内容

YYAsyncLayer主要有3个类.

1, YYTransaction,负责将 YYAsyncLayer委托的绘制任务在RunLoop空闲时执行.

2, YYSentine, 是一个线程安全的计数器,在进行队列分配和任务取消时作为参考使用

3, YYAsyncLayer, 将其替换为View的Layer类,实现异步绘制

YYTransaction

+ (YYTransaction *)transactionWithTarget:(id)target selector:(SEL)selector;方法创建委托对象.

- (void)commit;方法将委托对象存储在一个全局Set中,在空闲时回调.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void YYTransactionSetup() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
transactionSet = [NSMutableSet new];
CFRunLoopRef runloop = CFRunLoopGetMain();
CFRunLoopObserverRef observer;

observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopBeforeWaiting | kCFRunLoopExit,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
});
}

空闲回调block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 因为该对象还要被存放至集合中,当子类实现了isEqual方法时,则同时也要实现 hash方法.

- (NSUInteger)hash {
long v1 = (long)((void *)_selector);
long v2 = (long)_target;
return v1 ^ v2;
}

- (BOOL)isEqual:(id)object {
if (self == object) return YES;
if (![object isMemberOfClass:self.class]) return NO;
YYTransaction *other = object;
return other.selector == _selector && other.target == _target;
}

在这里重载 isEqual方法,确保不会将具有相同target和selector的委托对象放入Set中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static NSMutableSet *transactionSet = nil;

static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (transactionSet.count == 0) return;
NSSet *currentSet = transactionSet;
transactionSet = [NSMutableSet new];
[currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

[transaction.target performSelector:transaction.selector];
#pragma clang diagnostic pop
}];
}

创建唯一的 主线程 RunLoop观察者,在RunLoop进入kCFRunLoopBeforeWaiting 或 退出时 将委托方法调用.

YYSentine

YYSentine的实现比较简单,主要是对 OSAtomicIncrement32() 函数的封装, 改函数为一个线程安全的计数器, 它会会保证在 数自增后再对其访问, 在这个框架里他是用来 作为绘制任务是否被取消的参照物的~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import "YYSentinel.h"
#import <libkern/OSAtomic.h>

@implementation YYSentinel {
int32_t _value;
}

- (int32_t)value {
return _value;
}

- (int32_t)increase {
return OSAtomicIncrement32(&_value);
}
@end

YYAsyncLayer

队列准备

dispatch_queue_t YYAsyncLayerGetDisplayQueue() {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#ifdef YYDispatchQueuePool_h
return YYDispatchQueueGetForQOS(NSQualityOfServiceUserInitiated);
#else
#define MAX_QUEUE_COUNT 16
static int queueCount;
static dispatch_queue_t queues[MAX_QUEUE_COUNT];
static dispatch_once_t onceToken;
static int32_t counter = 0;
dispatch_once(&onceToken, ^{

//queueCount = 运行该进程的系统的处于激活状态的处理器数量,
queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
//确保 0<queueCount<16
queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;

//创建指定数量的 串行队列 存放在队列数组中
if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
for (NSUInteger i = 0; i < queueCount; i++) {
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
queues[i] = dispatch_queue_create("com.ibireme.yykit.render", attr);
}
} else {
for (NSUInteger i = 0; i < queueCount; i++) {
queues[i] = dispatch_queue_create("com.ibireme.yykit.render", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
}
}
});

// 此为线程安全的自增计数,每调用一次,+1
int32_t cur = OSAtomicIncrement32(&counter);

NSLog(@"cur:%d counter:%d",cur,counter);

//返回合适的队列
if (cur < 0) cur = -cur;
return queues[(cur) % queueCount];
#undef MAX_QUEUE_COUNT
#endif
}

准备若干的串行队列~,将绘制任务分给不同的串行队列, 这里之所以 队列数 和 处理器数 匹配. 不创建过多无效队列.

1
2
3
4
5
6
7
8
// 释放队列
static dispatch_queue_t YYAsyncLayerGetReleaseQueue() {
#ifdef YYDispatchQueuePool_h
return YYDispatchQueueGetForQOS(NSQualityOfServiceDefault);
#else
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
#endif
}

低优先级的全局队列作为 对象的释放队列,

代理方法

YYAsyncLayer的代理方法需要返回一个 DisplayTask对象, 任务对象中包括3个block.分别为willDisplay , display , didDisplay.在绘制的不同阶段执行

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (instancetype)init {
self = [super init];
static CGFloat scale; //global
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
scale = [UIScreen mainScreen].scale;
});
self.contentsScale = scale;
//默认异步,每个图层都配置一个计数器
_sentinel = [YYSentinel new];
_displaysAsynchronously = YES;
return self;
}

//dealloc时 取消绘制
- (void)dealloc {
[_sentinel increase];
}

//在再次绘制时,取消上次绘制任务
- (void)setNeedsDisplay {
[self _cancelAsyncDisplay];
[super setNeedsDisplay];
}
- (void)display {
//这个我看不懂~为啥要再赋值一遍
super.contents = super.contents;
[self _displayAsync:_displaysAsynchronously];
}

绘制方法

没有绘制任务

1
2
3
4
5
6
7
if (!task.display) {
if (task.willDisplay) task.willDisplay(self);
self.contents = nil;
if (task.didDisplay) task.didDisplay(self, YES);
///执行完其他非空block后 返回
return;
}

异步绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
if (task.willDisplay) task.willDisplay(self);
YYSentinel *sentinel = _sentinel;

int32_t value = sentinel.value;

//判断是否要取消的block, 在图层的dealloc方法,取消绘制方法中 和 同步绘制方法中 进行线程安全的自增操作. 在调用该block时 若block截取的变量value与对象中value中的值不一致时,则表明当前任务以被取消
BOOL (^isCancelled)(void) = ^BOOL() {
return value != sentinel.value;
};

CGSize size = self.bounds.size;
BOOL opaque = self.opaque;
CGFloat scale = self.contentsScale;

// 当图层宽度 或 高度小于 1时 (此时没有绘制意义)
if (size.width < 1 || size.height < 1) {
CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
self.contents = nil;

//当图层内容为图像时,讲释放操作留在 并行释放队列中进行
if (image) {
dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
CFRelease(image);
});
}
if (task.didDisplay) task.didDisplay(self, YES);
return;
}

///为正常情况
dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
//若发生取消操作,则取消绘制
if (isCancelled()) return;


UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
CGContextRef context = UIGraphicsGetCurrentContext();


task.display(context, size, isCancelled);

//若取消 则释放资源,取消绘制
if (isCancelled()) {
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}

//将上下文转换为图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

//若取消 则释放资源,取消绘制
if (isCancelled()) {
dispatch_async(dispatch_get_main_queue(), ^{
if (task.didDisplay) task.didDisplay(self, NO);
});
return;
}

///主线程异步 进行最后的绘制操作
dispatch_async(dispatch_get_main_queue(), ^{
if (isCancelled()) {
if (task.didDisplay) task.didDisplay(self, NO);
} else {
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);
}
});
});

YYAsyncLayer 是通过创建异步创建图像Context在其绘制,最后再主线程异步添加图像 从而实现的异步绘制.同时,在绘制过程中 进行了多次进行取消判断,以免额外绘制.

同步绘制

同步绘制就是直接绘制就好了~

1
2
3
4
5
6
7
8
9
[_sentinel increase];
if (task.willDisplay) task.willDisplay(self);
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
CGContextRef context = UIGraphicsGetCurrentContext();
task.display(context, self.bounds.size, ^{return NO;});
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.contents = (__bridge id)(image.CGImage);
if (task.didDisplay) task.didDisplay(self, YES);

使用

替换当前View的Layer

1
2
3
+ (Class)layerClass {
return YYAsyncLayer.class;
}

修改需要属性时 进行重绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)setText:(NSString *)text {
_text = text.copy;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)setFont:(UIFont *)font {
_font = font;
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)layoutSubviews {
[super layoutSubviews];
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}

- (void)contentsNeedUpdated {
[self.layer setNeedsDisplay];
}

实现代理方法 完成绘制任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {

NSString *text = _text;
UIFont *font = _font;

YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer *layer) {

};

task.display = ^(CGContextRef context, CGSize size, BOOL(^isCancelled)(void)) {
if (isCancelled()) return;
//在这里由于绘制文字会颠倒
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
}];
NSAttributedString* str = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName:_font}];
CGContextSetTextPosition(context, 0, font.pointSize);
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)str);
CTLineDraw(line, context);
};

task.didDisplay = ^(CALayer *layer, BOOL finished) {
if (finished) {
// finished
} else {
// cancelled
}
};

return task;
}

效果

作为对照,添加UILabel进行对比试验,重写其 -(void)drawRect:方法打印输出比较.
运行结果

由此可知: 同步绘制任务(la2) 在viewDidAppear前完成绘制, 而AsyncLayer则在这之后再开始绘制任务,切绘制方法在异步执行.

原生API对比.

关于异步绘制,iOS6 为CALayer添加了新的API drawsAsynchronously 属性.当你设置 drawsAsynchronously = YES 后,-drawRect: 和 -drawInContext: 函数依然实在主线程调用的。但是所有的Core Graphics函数(包括UIKit的绘制API,最后其实还是Core Graphics的调用)不会做任何事情,而是所有的绘制命令会被在后台线程处理。

这种方式就是先记录绘制命令,然后在后台线程执行。为了实现这个过程,更多的事情不得不做,更多的内存开销。最后只是把一些工作从主线程移动出来。这个过程是需要权衡,测试的。

这个可能是代价最昂贵的的提高绘制性能的方法,也不会节省很多资源。

相比之下,AsyncLaye的性能会好一些, 但麻烦的是 绘制实现要自己写~

错误提示

在我使用的版本(1.0)中 异步绘制的bitmap的scale为1.0 因为+(id)defaultValueForKey:(NSString *)key 方法,所以在使用时 注意修改~~~ 不然显示的画面会有模糊感

CATALOG
  1. 1. YYAsyncLayer 学习
  2. 2. 简介
  3. 3. 组件内容
    1. 3.1. YYTransaction
    2. 3.2. YYSentine
    3. 3.3. YYAsyncLayer
      1. 3.3.1. 队列准备
      2. 3.3.2. 代理方法
      3. 3.3.3. 初始化
      4. 3.3.4. 绘制方法
        1. 3.3.4.1. 没有绘制任务
        2. 3.3.4.2. 异步绘制
        3. 3.3.4.3. 同步绘制
  4. 4. 使用
    1. 4.1. 替换当前View的Layer
    2. 4.2. 修改需要属性时 进行重绘制
    3. 4.3. 实现代理方法 完成绘制任务
  5. 5. 效果
  6. 6. 原生API对比.
  7. 7. 错误提示